<?php

/**
 * 分布式表
 * W:吴振球
 *
 * 1.目标：通过读取配置文件，实现分库分表规则
 *
 * 2.实例化：操作与Table.php一样，如：
 *     SupportTable::instance->get(...);
 *
 * 3.配置文件:
 *      需要分库分表时，需设置好配置文件如：SupportTable的配置文件如下（conf/app_share/develop.db.php）
 * $post_sharding = array (
'db'              => 1,  //库数量
'table'           => 4,  //表数量
);
 * 4.配置名：
 *      配置名可在Dao层修改，对应配置文件返回的名字
 *   private $DDB_conf_name = 'post_sharding';
 *
 * 5.Dao层对库名和表名的要求:
 *   Dao层库名：dev_csp，+配置文件（4库）后最终库名：dev_csp_0,dev_csp_1,dev_csp_2,dev_csp_3,
 *   Dao层表名：post_suppert，+配置文件（4表）后最终表名：post_suppert_0,post_suppert_1,post_suppert_2,post_suppert_3,
 *
 * 6.总结：
 *      配置文件+Dao层配置名=DDBTable
 *
 */
namespace Lavender\Dao;

use Lavender\Core;
use Lavender\Errno;

/**
 * table access class
 */
abstract class DDBTable
{
    /**
     * database system
     *
     * @var string
     */
    protected $driver = 'mysql';

    /**
     * database config name
     *
     * @var string
     */
    protected $database = '';

    /**
     * table
     *
     * @var string
     */
    protected $table = '';

    /**
     * first key field name
     *
     * @var string
     */
    protected $first_key = 'id';

    /**
     * use to cache query data
     *
     * @var array
     */
    protected $single_cache = array();

    /**
     * fields to type map
     *
     * @var array
     */
    protected $field_definitions = array();

    // singletion pattern
    protected $db_handle;

    /**
     * all instances cache
     *
     * @var array
     */
    private static $instances = array();

    // set config name
    protected $DDB_conf_name = 'post_sharding';

    /**
     * Sets of table=>id
     * array(
     *     table1=>array(id1,id2,id3),
     *     table2=>array(......),
     * );
     */
    protected  $tables = array();

    /**
     * Sets of database=>table
     * array(
     *     db1=>array(tb1,tb2,tb3),
     *     db2=>array(......),
     * );
     */
    protected  $dbs = array();

    /***
     * @var bool 同步开关
     */
    public $is_sync=true;

    //
    public function __construct()
    {
        //
    }

    /**
     * sharding rule
     *
     * @param string|int $sharding_id
     */
    public function sharding_rule($sharding_id)
    {
        if (empty ($sharding_id)) {
            throw new Exception ('param missed', Errno::PARAM_MISSED);
        }
        // if not array ,array $sharding_id
        if (!is_array($sharding_id)) {
            $sharding_id = array(
                $sharding_id
            );
        }
        $sharding_id = array_unique($sharding_id);
        $conf = Core::get_config('db', $this->DDB_conf_name);
        foreach ($sharding_id as $vs) {
            // set database name
            $index = $vs % $conf ['db'];
            $database_suffix = '_' . $index;

            // set table name
            $n = ($vs / $conf ['db']) % $conf ['table'];
            $table = $this->table . '_' . $n;

            // Sets of database and table
            $dbs [] = array(
                $database_suffix => $table
            );
            // Sets of table and id
            $tables [] = array(
                $table => $vs
            );
        }
        $this->dbs = self::array_two($dbs);
        $this->tables = self::array_two($tables);
    }

    /**
     * make rule for an array
     *
     * @param array $two
     * @return multitype:
     */
    public function array_two(array $two)
    {
        foreach ($two as $k => $v) {
            foreach ($v as $k1 => $v1) {
                $ks [] = $k1;
            }
        }
        // unique array key
        $ks = array_unique($ks);
        foreach ($two as $k => $v) {
            foreach ($ks as $v3) {
                if ($two [$k] [$v3] != null) {
                    $res [$v3] [] = $two [$k] [$v3];
                }
            }
        }
        foreach ($res as $k4 => $v4) {
            // unique array value
            $result [$k4] = array_unique($res [$k4]);
        }
        return $result;
    }

    /**
     * get instance singletion
     *
     * @return Table
     */
    public static function instance()
    {
        $class = get_called_class();
        if (empty (self::$instances [$class])) {
            self::$instances [$class] = new $class ();
        }
        return self::$instances [$class];
    }

    /**
     * multipie get
     *
     * @param mixed $ids
     *            id or id list
     * @param array $fields
     * @param array $filter
     * @param string $order
     * @param int $offset
     * @param int $length
     *
     * @return array
     */
    public function get($ids, $fields = array(), array $filter = array(), $order = null, $offset = null, $length = null)
    {
        if (empty($ids)) {
            if (array_key_exists($this->first_key, $filter)) {
                $ids = $filter[$this->first_key];
            } else {
                return array();
            }

        }
        // 得到分库分表规则
        self::sharding_rule($ids);
        // 循环实例化数据库
        foreach ($this->dbs as $database_suffix => $tables) {
            $this->db_handle = Core::get_database($this->driver, $this->database, $database_suffix);
            foreach ($tables as $table) {
                // 得到当前$table下的的ids
                $condition = $this->build_condition($this->tables [$table], $filter);
                $res [] = $this->db_handle->get($table, $condition, $fields, $order, $offset, $length);
            }
        }
        //整理结果集
        foreach ($res as $k => $v) {
            foreach ($v as $k2 => $v2) {
                $result [] = $v2;
            }
        }
        return $result;
    }

    public function get_by_filter($ids, $fields = array(), array $filter = array(), $order = null, $offset = null, $length = null)
    {
        $condition = $this->build_condition_filter($filter);
        return $this->db_handle->get($this->table, $condition, $fields, $order, $offset, $length);
    }

    /**
     * single record get
     *
     * @param int $id
     * @param array $filter
     *            filter condition
     * @param string $order
     *
     * @return mixed
     */
    public function get_single($id, array $filter = array(), $order = null)
    {
        if ($id !== null && !is_numeric($id)) {
            throw new Exception ("first_key's value invalid", Errno::PARAM_INVALID);
        }

        if (is_null($id) && empty ($filter)) {
            throw new Exception ("id and filter is empty", Errno::PARAM_INVALID);
        }

        // single
        if (isset ($this->single_cache [$id])) {
            return $this->single_cache [$id];
        }

        $condition = $this->build_condition($id, $filter);
        $items = $this->db_handle->get($this->table, $condition, array(), $order, 1);
        if (empty ($items)) {
            return null;
        }

        // cache to process cache
        $this->single_cache [$id] = $items [0];

        return $items [0];
    }

    /**
     * insert record
     *
     * @param int $id
     * @param array $record
     *
     * @return void
     */
    public function add($id, array $record)
    {
        if (!is_array($record)) {
            throw new Exception ('param error', Errno::PARAM_INVALID);
        }
        if ($id == null) {
            if (array_key_exists($this->first_key, $record)) {
                $id = $record[$this->first_key];
            } else {
                throw new Exception ("first_key's value invalid", Errno::PARAM_INVALID);
            }
        }
        if (!is_numeric($id)) {
            throw new Exception ("first_key's value invalid", Errno::PARAM_INVALID);
        }
        // 得到分库分表规则
        self::sharding_rule($id);
        // 循环实例化数据库
        foreach ($this->dbs as $database_suffix => $tables) {
            $this->db_handle = Core::get_database($this->driver, $this->database, $database_suffix);
            foreach ($tables as $table) {
                $record [$this->first_key] = $this->tables [$table] [0];
                $this->db_handle->insert($table, $record);
            }
        }
        //数据同步
        self::set_sync_data(get_called_class(),__FUNCTION__,func_get_args());
    }

    /**
     * set to a record
     *
     * @param int $id
     * @param array $record
     *
     * @return void
     */
    public function set($id, array $record)
    {
        if (empty ($record) || !is_array($record)) {
            throw new Exception ('param "$record" is empty', Errno::PARAM_INVALID);
        }
        if ($id == null) {
            if (array_key_exists($this->first_key, $record)) {
                $id = $record[$this->first_key];
            } else {
                throw new Exception ("first_key's value invalid", Errno::PARAM_INVALID);
            }
        }
        if (!is_numeric($id)) {
            throw new Exception ("first_key's value invalid", Errno::PARAM_INVALID);
        }
        // 获取分库分表规则
        self::sharding_rule($id);
        // 循环实例化数据库
        foreach ($this->dbs as $database_suffix => $tables) {
            $this->db_handle = Core::get_database($this->driver, $this->database, $database_suffix);
            foreach ($tables as $table) {
                // 只有一条记录
                $record [$this->first_key] = $this->tables [$table] [0];
                $this->db_handle->insert_or_update($table, $record, $record);
            }
        }
        //数据同步
        self::set_sync_data(get_called_class(),__FUNCTION__,func_get_args());
    }

    /**
     * update a record
     *
     * @param int $id
     * @param array $record
     * @param array $filter
     *
     * @return int the affected rows
     */
    public function update($id, $record, array $filter = array())
    {
        if ($id == null) {
            if (array_key_exists($this->first_key, $filter)) {
                $id = $filter[$this->first_key];
            } else {
                throw new Exception ("first_key's value invalid", Errno::PARAM_INVALID);
            }
        }
        if (!is_numeric($id)) {
            throw new Exception ("first_key's value invalid", Errno::PARAM_INVALID);
        }
        if ($id !== null && !is_numeric($id)) {
            throw new Exception ("first_key's value invalid", Errno::PARAM_INVALID);
        }

        // 获取分库分表规则
        self::sharding_rule($id);
        // 循环实例化数据库
        foreach ($this->dbs as $database_suffix => $tables) {
            $this->db_handle = Core::get_database($this->driver, $this->database, $database_suffix);
            foreach ($tables as $table) {
                // 单条数据处理
                $condition = $this->build_condition($this->tables [$table] [0], $filter);
                $this->db_handle->update($table, $record, $condition);
            }
        }

        //数据同步
        self::set_sync_data(get_called_class(),__FUNCTION__,func_get_args());

        return $this->db_handle->get_affected_rows();
    }

    /**
     * delete
     *
     * @param int $id
     * @param array $filter
     *
     * @return int the affected rows
     */
    public function delete($id, array $filter = array())
    {
        if ($id == null) {
            if (array_key_exists($this->first_key, $filter)) {
                $id = $filter[$this->first_key];
            } else {
                throw new Exception ("first_key's value invalid", Errno::PARAM_INVALID);
            }
        }
        // 获取分库分表规则
        self::sharding_rule($id);
        // 循环实例化数据库
        foreach ($this->dbs as $database_suffix => $tables) {
            $this->db_handle = Core::get_database($this->driver, $this->database, $database_suffix);
            foreach ($tables as $table) {
                // table下的多条ID
                $condition = $this->build_condition($this->tables [$table], $filter);
                $this->db_handle->delete($table, $condition);
            }
        }

        //数据同步
        self::set_sync_data(get_called_class(),__FUNCTION__,func_get_args());

        return $this->db_handle->get_affected_rows();
    }

    /**
     * increment to field
     *
     * @param int $id
     * @param string $field
     * @param int $num
     * @param array $filter
     *
     * @return int the affected rows
     */
    public function increment($id, $field, $num, array $update_item = array())
    {
        if (!$id || !is_numeric($id)) {
            throw new Exception ("first_key's value invalid", Errno::PARAM_INVALID);
        }

        // check field
        if (empty ($field) || !is_string($field) || !$this->db_handle->check_name($field)) {
            throw new Exception ('param error,field ({$field}) invalid', Errno::PARAM_INVALID);
        }

        if ($num >= 0) {
            // build insert item
            $insert_item = $update_item;
            $insert_item [$field] = $num;
            if (!is_null($id)) {
                $insert_item [$this->first_key] = $id;
            }

            $insert = $this->db_handle->build_insert_string($insert_item);

            // base sql
            $sql = "INSERT INTO `{$this->table}` ({$insert['fields']}) VALUES ({$insert['values']}) ON DUPLICATE KEY UPDATE `{$field}` = `{$field}` + $num";

            // update item
            if (!empty ($update_item)) {
                $update = $this->db_handle->build_update_string($update_item);
                $sql .= ",{$update}";
            }
        } else {
            $sql = "UPDATE `{$this->table}` SET `{$field}` = `{$field}` + $num";

            // update item
            if (!empty ($update_item)) {
                $update = $this->db_handle->build_update_string($update_item);
                $sql .= ",{$update}";
            }

            $sql .= " WHERE id=" . $id;
        }

        $this->db_handle->execute($sql);
        return $this->db_handle->get_affected_rows();
    }

    /**
     * count
     *
     * @param int $id
     * @param array $filter
     *            filter condition
     *
     * @return int
     */
    public function count($id, array $filter = array())
    {
        if ($id == null) {
            if (array_key_exists($this->first_key, $filter)) {
                $id = $filter[$this->first_key];
            } else {
                throw new Exception ("first_key's value invalid", Errno::PARAM_INVALID);
            }
        }
        if ($id !== null && !is_numeric($id)) {
            throw new Exception ("first_key's value invalid", Errno::PARAM_INVALID);
        }
        // 得到分库分表规则
        self::sharding_rule($id);
        // 循环实例化数据库
        foreach ($this->dbs as $database_suffix => $tables) {
            $this->db_handle = Core::get_database($this->driver, $this->database, $database_suffix);
            foreach ($tables as $table) {
                // 得到当前$table下的的ids
                $condition = $this->build_condition($this->tables[$table][0], $filter);
                return $this->db_handle->count($table, $condition);
            }
        }
    }

    /**
     * build conditon by id & filter
     *
     * @param int $id
     * @param array $filter
     *
     * @return string
     */
    protected function build_condition($id, array $filter)
    {
        $condition_items = array();

        if ($id !== null) {
            if (is_array($id)) {
                $condition_items [] = "`{$this->first_key}` IN ('" . implode("','", $id) . "')";
            } else {
                $condition_items [] = "`{$this->first_key}`=" . $id;
            }
        }

        if (!empty ($filter)) {
            foreach ($filter as $k => $v) {
                if (!$this->db_handle->check_name($k)) {
                    throw new Exception ("filter field name verify failed,name:\'{$k}\'", Errno::PARAM_INVALID);
                }

                $v = $this->db_handle->escape($v);
                $condition_items [] = "`{$k}`='{$v}'";
            }
        }

        return implode(' AND ', $condition_items);
    }

    /**
     * build filter
     *
     * @param int $id
     * @param array $filter
     *
     * @return string
     */
    protected function build_condition_filter(array $filter)
    {
        $condition_items = array();
        if (!empty ($filter)) {
            foreach ($filter as $k => $v) {
                if (!$this->db_handle->check_name($k)) {
                    throw new Exception ("filter field name verify failed,name:\'{$k}\'", Errno::PARAM_INVALID);
                }
                $condition_items [] = "{$v}";
            }
        }

        return implode(' AND ', $condition_items);
    }

    protected   function  set_sync_data($called_class,$function,array $params){
        //数据同步
//        if ($this->is_sync) {
//            $sync_table=Core::get_config('const','sync_table');
//            if ($sync_table && in_array($called_class,$sync_table)){
//                \Golo\Api\DataSync::send(L_APP_NAME,$called_class,$function, $params);
//            }
//        }
    }
}

